xtask\tasks\fmt\house_rules/
trailing_newline.rs1use anyhow::anyhow;
5use fs_err::File;
6use fs_err::OpenOptions;
7use std::io::Read;
8use std::io::Seek;
9use std::io::SeekFrom;
10use std::io::Write;
11use std::path::Path;
12
13pub fn check_trailing_newline(path: &Path, fix: bool) -> anyhow::Result<()> {
14 let ext = path
15 .extension()
16 .and_then(|e| e.to_str())
17 .unwrap_or_default();
18
19 if !matches!(
20 ext,
21 "c" | "md" | "proto" | "py" | "rs" | "sh" | "toml" | "txt" | "yml" | "js" | "ts"
22 ) {
23 return Ok(());
24 }
25
26 if path.file_name().unwrap() == "toc.yml" {
28 return Ok(());
29 }
30
31 let mut f = OpenOptions::new().read(true).write(fix).open(path)?;
32 f.seek(SeekFrom::End(-2))?;
33 let mut b = [0; 2];
34 f.read_exact(&mut b)?;
35
36 let missing_single_trailing_newline = !(b[0] != b'\n' && b[1] == b'\n');
37
38 if missing_single_trailing_newline {
39 if fix {
40 let truncate_to = find_first_trailing_nl(&mut f)?;
41 f.set_len(truncate_to)?;
42 f.seek(SeekFrom::End(0))?;
43 writeln!(f)?;
44 } else {
45 return Err(anyhow!(
47 "missing single trailing newline in {}",
48 path.display()
49 ));
50 }
51 }
52
53 Ok(())
54}
55
56fn find_first_trailing_nl(f: &mut File) -> std::io::Result<u64> {
59 const BLOCK_SIZE: u64 = 512;
60
61 let mut pos = f.seek(SeekFrom::End(0))?;
62 let mut file_block = [0; BLOCK_SIZE as usize];
63 while pos != 0 {
64 let new_pos = pos.saturating_sub(BLOCK_SIZE);
65 let delta = pos - new_pos;
66 pos = new_pos;
67
68 let file_block = &mut file_block[..delta as usize];
69 f.seek(SeekFrom::Start(pos))?;
70 f.read_exact(file_block)?;
71
72 let num_trailing_newlines =
73 file_block.iter().rev().take_while(|x| **x == b'\n').count() as u64;
74
75 match num_trailing_newlines {
76 0 => {
77 pos += delta;
79 break;
80 }
81 n if n == delta => {
82 }
84 n => {
85 pos += delta - n;
87 break;
88 }
89 }
90 }
91
92 Ok(pos)
93}